#! /usr/bin/env python3
# vim:fenc=utf-8
#
# Copyright © 2025 wuzifang <2992466620@qq.com>
#
# Distributed under terms of the MIT license.

import subprocess
import time
import re
import threading
import subprocess
import re

import common_func
import logging
import eth_interface

from threading import Timer
from eth_interface import RouteManager
from common_func import print_with_location
from common_func import ConnectionStatus
from enum import Enum, auto


class PPP:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            with cls._lock:
                if not cls._instance:
                    cls._instance = super(PPP, cls).__new__(cls)
        return cls._instance

    def __init__(self, status_callback=None, check_interval=3):

        if hasattr(self, "_initialized") and self._initialized:
            return  # 防止重复初始化

        self.process = None
        self.status = ConnectionStatus.DISCONNECTED
        self._monitor_thread = None
        self._stop_event = threading.Event()
        self.check_interval = check_interval  # 检查间隔（秒）
        self.status_callback = status_callback  # 保存外部回调函数
        self._lock = threading.Lock()
        self.reconnect_count = 0
        self.manager = RouteManager(check_interval=5)

    def start_monitoring(self):
        if self._monitor_thread is None:
            self._stop_event.clear()
            self._monitor_thread = threading.Thread(
                target=self._monitor_ppp_status,
                daemon=True,  # 设置为守护线程（主退出时自动结束）
            )
            self.reconnect_count = 0
            self._monitor_thread.start()

    def stop_monitoring(self):
        if self._monitor_thread:
            self._stop_event.set()
            self._monitor_thread.join()
            self._monitor_thread = None

    def _monitor_ppp_status(self):
        while not self._stop_event.is_set():
            if self.get_ppp_ip() is None:
                print_with_location("PPP disconnected, attempting to reconnect...")
                self.reconnect()
            time.sleep(self.check_interval)

    def dial_ppp(self):
        self.__check_ppp_status__(ConnectionStatus.CONNECTING)
        try:
            # Run pppd as a background process from the /etc/ppp/peers directory
            process = subprocess.Popen(
                ["pppd", "call", "quectel-ppp"],
                cwd="/etc/ppp/peers",  # Set working directory
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )
            print_with_location(
                "Dial command started from /etc/ppp/peers, waiting for connection..."
            )
            time.sleep(5)  # wait a bit for pppd to establish connection
        except Exception as e:
            self.__check_ppp_status__(ConnectionStatus.FAILED, 300, "4g connect falied")
            print_with_location(f"Dial failed:{str(e)}")

    def kill_ppp(self):
        try:
            ps = subprocess.run(
                "ps aux | grep 'pppd call quectel-ppp'",
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )

            if ps.stdout.strip() == "":
                print_with_location("No pppd process found.")
                self.__check_ppp_status__(ConnectionStatus.DISCONNECTED)
                return None

            lines = ps.stdout.strip().splitlines()
            for line in lines:
                pid = line.split()[0]
                subprocess.run(f"kill {pid}", shell=True)
                print_with_location(f"Killed pppd process with PID: {pid}")
            self.__check_ppp_status__(ConnectionStatus.DISCONNECTED)

        except Exception as e:
            print_with_location(f"Failed to kill pppd process:{ str(e)}")
            self.__check_ppp_status__(ConnectionStatus.FAILED, code=300, msg=str(e))

    def get_ppp_ip(self):
        try:
            result = subprocess.run(
                "ifconfig ppp0",
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )

            if "ppp0" not in result.stdout:
                print_with_location("PPP interface not found.")
                self.__check_ppp_status__(
                    ConnectionStatus.FAILED, 300, "4g connect exception"
                )

                return None

            # Look for "inet addr:<IP>" format
            match = re.search(r"inet addr:(\d+\.\d+\.\d+\.\d+)", result.stdout)
            if match:
                ip = match.group(1)
                print_with_location(f"PPP local IP address: {ip}")
                self.__check_ppp_status__(ConnectionStatus.CONNECTED)
                # 添加一条默认路由
                self.add_default_route()

                return ip
            else:
                print_with_location("IP address not found.")
                self.__check_ppp_status__(
                    ConnectionStatus.FAILED, 300, "4g connect exception"
                )
                return None

        except Exception as e:
            print_with_location(f"Failed to get IP address:{str(e)}")
            return None

    def get_csq_value(self):
        try:
            # 查询下当前默认路由是否只有一个ppp0，如果不是返回0
            if self._check_default_is_ppp() is False:
                return 0

            # Send AT+CSQ command
            subprocess.run(
                'echo "AT+CSQ" | tee /dev/ttyUSB2',
                shell=True,
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )

            # Start cat process with 2-second timeout
            proc = subprocess.Popen(
                ["cat", "/dev/ttyUSB2"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )

            # Set 2-second timeout to kill the process
            timer = Timer(0.5, proc.kill)
            timer.start()

            # Read output (blocks until process ends or times out)
            stdout, _ = proc.communicate()
            timer.cancel()  # Cancel if completed early

            # Parse CSQ value (even if timed out)
            match = re.search(r"\+CSQ:\s*(\d+),", stdout)
            if match:
                csq = int(match.group(1))
                print_with_location(f"CSQ value obtained: {csq}")
                return csq
            else:
                print_with_location("CSQ value not found. Output:", stdout)
                return None

        except Exception as e:
            print_with_location(f"Error occurred: {e}")
            return None
            # 执行两条shell指令，获取rssi

    def __check_ppp_status__(self, new_status, code: int = 0, msg: str = None):
        try:
            with self._lock:
                self.status = new_status
                if self.status_callback:
                    self.status_callback(
                        status=new_status.value,
                        hwt_ype="4g",
                        ssid=None,
                        error_msg=msg,
                        error_code=code,
                    )
        except Exception as e:
            print_with_location(f"Failed chande status :{str(e)}")
            return None

    def reconnect(self):
        try:
            if self.reconnect_count >= 1:
                return None
            self.reconnect_count += 1
            self.kill_ppp()
            time.sleep(1)
            self.dial_ppp()
            ip = self.get_ppp_ip()
            if ip is None:
                self.reconnect()
            self.reconnect_count = 0
        except Exception as e:
            print_with_location(f"Failed to reconnect: str(e)")
            self.reconnect_count = 0

    def add_default_route(self):
        try:
            # Check existing default routes
            check_result = subprocess.run(
                "ip route show default",
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )

            # Check if any default route uses ppp0
            if "default dev ppp0" in check_result.stdout:
                print_with_location(
                    "Default route via ppp0 already exists. Skipping addition."
                )
                return True

            # Add the route if it doesn't exist
            subprocess.run(
                "ip route add default dev ppp0  metric 50",
                shell=True,
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )
            print_with_location("add defaultroute success: default dev ppp0")
            return True

        except subprocess.CalledProcessError as e:
            print_with_location(f"add defaultroute failed: {e.stderr}")
            return False

    def _check_default_is_ppp(self):
        try:
            # 获取所有路由信息
            check_result = subprocess.run(
                "ip route show",
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                universal_newlines=True,
            )

            # 分割输出为行
            routes = check_result.stdout.strip().split("\n")

            # 统计默认路由数量
            default_routes = [r for r in routes if r.startswith("default")]

            # 检查条件：
            # 1. 有且仅有一个默认路由
            # 2. 该路由使用ppp0接口
            if len(default_routes) == 1 and "dev ppp0" in default_routes[0]:
                return True
            return False

        except subprocess.CalledProcessError as e:
            print_with_location(f"check default iproute failed: {e.stderr}")
            return False

    def connect(self):
        self.stop_monitoring()
        self.dial_ppp()
        self.start_monitoring()
        self.get_ppp_ip()

    def disconnect(self):
        self.stop_monitoring()
        self.kill_ppp()

    @property
    def get_status(self):
        with self._lock:
            return self.status
